<!DOCTYPE html>
<html>
<head>
  <style>
    html, body {
      margin: 0;
      overflow: hidden;
      height: 100%;
    }
  </style>
  <title>WebGL training - Phong Shading</title>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script type="module">
    import {
      GUI,
      vec3,
      quat,
      mat4,
      toHighDPI,
      createProgram,
      createBuffer,
      createVertexArray,
      bindUniformBlock,
      Camera,
      createCube,
      createSphere,
    } from '../js/bundle.js';

    const config = {
      clearColor: [0, 0, 0],
      ambient: true,
      diffuse: true,
      specular: true,
      ambientColor: [0.3 * 255, 0.3 * 255, 0.3 * 255],
      ambientColorValue: [0.3, 0.3, 0.3],
      lightDirectionX: -1,
      lightDirectionY: -2,
      lightDirectionZ: -3,
      lightDirection: [-1, -1, -1],
      lightColor: [1 * 255, 1 * 255, 1 * 255],
      lightColorValue: [1, 1, 1],
    };

    function setLightDirection() {
      vec3.set(
        config.lightDirection,
        config.lightDirectionX,
        config.lightDirectionY,
        config.lightDirectionZ,
      );
      vec3.normalize(config.lightDirection, config.lightDirection);
    }
    setLightDirection();
    const meshes = [
      {
        geometry: createCube(),
        material: {
          color: [0.5, 0.5, 0.5],
        },
        tx: -1.5,
        ty: 0.0,
        tz: 0.0,
        sx: 1.0,
        sy: 1.0,
        sz: 1.0,
        rx: 0.0,
        ry: 0.0,
        rz: 0.0,
        translate: vec3.create(),
        scale: vec3.create(),
        quat: quat.create(),
        matrix: mat4.create(),
        normalMatrix: mat4.create(),
      },
      {
        geometry: createSphere(),
        material: {
          color: [0.5, 0.5, 0.5],
        },
        tx: 1.5,
        ty: 0.0,
        tz: 0.0,
        sx: 1.0,
        sy: 1.0,
        sz: 1.0,
        rx: 0.0,
        ry: 0.0,
        rz: 0.0,
        translate: vec3.create(),
        scale: vec3.create(),
        quat: quat.create(),
        matrix: mat4.create(),
        normalMatrix: mat4.create(),
      },
    ];
    const canvas = document.getElementById('canvas');
    // 01: create WebGL context
    const gl = canvas.getContext('webgl2');
    // 02.1: create program from shader source
    const program = createProgram(gl, `#version 300 es
      precision highp float;
      layout(std140, column_major) uniform;

      layout(location = 0) in vec3 a_position;
      layout(location = 2) in vec3 a_normal;

      struct DirectionLight {
        vec3 direction;
        vec3 color;
        float ambient;
        float diffuse;
        float specular;
      };

      uniform Scene {
        uniform mat4 u_viewMatrix;
        uniform mat4 u_projectionMatrix;
        uniform vec3 u_eyePosition;
        uniform vec3 u_ambientColor;
        uniform DirectionLight u_directionLight;
      };

      uniform Model {
        uniform mat4 u_modelMatrix;
        uniform mat4 u_normalMatrix;
        uniform vec3 u_color;
      };

      out vec3 v_normal;
      out vec3 v_position;

      void main () {
        vec4 position = u_modelMatrix * vec4(a_position, 1.0);
        v_position = position.xyz;
        position = u_projectionMatrix * u_viewMatrix * position;
        gl_Position = position;
        v_normal = mat3(u_normalMatrix) * a_normal;
      }
    `, `#version 300 es
      precision highp float;
      layout(std140, column_major) uniform;

      struct DirectionLight {
        vec3 direction;
        vec3 color;
        float ambient;
        float diffuse;
        float specular;
      };

      uniform Scene {
        uniform mat4 u_viewMatrix;
        uniform mat4 u_projectionMatrix;
        uniform vec3 u_eyePosition;
        uniform vec3 u_ambientColor;
        uniform DirectionLight u_directionLight;
      };

      uniform Model {
        uniform mat4 u_modelMatrix;
        uniform mat4 u_normalMatrix;
        uniform vec3 u_color;
      };

      in vec3 v_normal;
      in vec3 v_position;
      out vec4 fragColor;

      void main () {
        vec3 baseColor = u_color;
        vec3 normal = normalize(v_normal);
        float diffuse = max(0.0, dot(-u_directionLight.direction, normal));
        float specular = 0.0;
        if (diffuse > 0.0) {
          vec3 eyeDirection = normalize(u_eyePosition - v_position);
          specular = pow(max(0.0, dot(reflect(u_directionLight.direction, normal), eyeDirection)), 64.0);
        }
        vec3 ambientColor = u_ambientColor * baseColor * u_directionLight.ambient;
        vec3 diffuseColor = diffuse * u_directionLight.color * baseColor * u_directionLight.diffuse;
        vec3 specularColor = specular * u_directionLight.color * u_directionLight.specular;
        fragColor = vec4(ambientColor + diffuseColor + specularColor, 1.0);
      }
    `);
    // 02.2: get uniform block locations
    // https://github.com/WebGLSamples/WebGL2Samples/blob/master/samples/buffer_uniform.html
    bindUniformBlock(gl, program, 'Scene', 0);
    bindUniformBlock(gl, program, 'Model', 1);
    const sceneUniformData = new Float32Array(16 + 16 + 4 + 4 + 4 + 4 + 4);
    const sceneUniformBuffer = createBuffer(gl, gl.UNIFORM_BUFFER, sceneUniformData, gl.DYNAMIC_DRAW);
    const modelUniformData = new Float32Array(16 + 16 + 4);
    const modelUniformBuffer = createBuffer(gl, gl.UNIFORM_BUFFER, modelUniformData, gl.DYNAMIC_DRAW);

    meshes.forEach((mesh) => {
      // 03: create vao
      mesh.vao = createVertexArray(gl, mesh.geometry);
      // calculate matrix
      vec3.set(mesh.translate, mesh.tx, mesh.ty, mesh.tz);
      vec3.set(mesh.scale, mesh.sx, mesh.sy, mesh.sz);
      quat.fromEuler(mesh.quat, mesh.rx, mesh.ry, mesh.rz);
      mat4.fromRotationTranslationScale(mesh.matrix, mesh.quat, mesh.translate, mesh.scale);
      mat4.invert(mesh.normalMatrix, mesh.matrix);
      // http://www.lighthouse3d.com/tutorials/glsl-12-tutorial/the-normal-matrix/
      // https://gamedev.stackexchange.com/questions/115135/opengl-glsl-matrix-in-uniform-block-confusion
      // https://www.gamedev.net/forums/topic/574897-help-with-uniform-buffer-objects/
      mat4.transpose(mesh.normalMatrix, mesh.normalMatrix);
    });

    // setup camera control
    const camera = new Camera();
    camera.attach(canvas);

    function render() {
      // 04: clean canvas before drawing
      gl.clearColor(
        config.clearColor[0] / 255,
        config.clearColor[1] / 255,
        config.clearColor[2] / 255,
        1,
      );
      // eslint-disable-next-line
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
      gl.enable(gl.DEPTH_TEST);

      // 05: use program
      gl.useProgram(program);

      /* gl.uniform1i(ambientUniform, config.ambient);
      gl.uniform1i(diffuseUniform, config.diffuse);
      gl.uniform1i(specularUniform, config.specular); */

      // 06: set scene uniform buffer
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, sceneUniformBuffer);
      let offset = 0;
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set
      sceneUniformData.set(camera.getViewMatrix(), offset);
      offset += 16;
      sceneUniformData.set(camera.getProjectionMatrix(), offset);
      offset += 16;
      sceneUniformData.set(camera.eye, offset);
      offset += 4;
      sceneUniformData.set(config.ambientColorValue, offset);
      offset += 4;
      sceneUniformData.set(config.lightDirection, offset);
      offset += 4;
      sceneUniformData.set(config.lightColorValue, offset);
      offset += 3;
      sceneUniformData[offset] = config.ambient ? 1 : 0;
      sceneUniformData[offset + 1] = config.diffuse ? 1 : 0;
      sceneUniformData[offset + 2] = config.specular ? 1 : 0;
      gl.bufferSubData(gl.UNIFORM_BUFFER, 0, sceneUniformData);

      meshes.forEach((mesh) => {
        // 07: set model uniform buffer
        gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, modelUniformBuffer);
        offset = 0;
        modelUniformData.set(mesh.matrix, offset);
        offset += 16;
        modelUniformData.set(mesh.normalMatrix, offset);
        offset += 16;
        modelUniformData.set(mesh.material.color, offset);
        gl.bufferSubData(gl.UNIFORM_BUFFER, 0, modelUniformData);

        // 08: bind vao
        gl.bindVertexArray(mesh.vao);

        // 09: draw triangles
        if (mesh.geometry.indices) {
          gl.drawElements(gl.TRIANGLES, mesh.geometry.count, gl.UNSIGNED_SHORT, 0);
        } else {
          gl.drawArrays(gl.TRIANGLES, 0, mesh.geometry.count);
        }
      });

      gl.useProgram(null);
    }

    // handle resize
    function resize() {
      const { clientWidth, clientHeight } = document.documentElement;
      canvas.width = clientWidth;
      canvas.height = clientHeight;
      // handle high DPI screen
      toHighDPI(canvas);
      // set viewport
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      render();
    }

    window.onresize = resize;
    resize();

    camera.onZoom = (scale) => {
      render();
    };
    camera.onPan = (x, y) => {
      render();
    };
    camera.onRotate = (x, y) => {
      render();
    };

    // config
    const gui = new GUI();
    gui.addColor(config, 'clearColor').onChange(() => {
      render();
    });
    const lightFolder = gui.addFolder('Light');
    lightFolder.add(config, 'ambient').onChange(() => {
      render();
    });
    lightFolder.add(config, 'diffuse').onChange(() => {
      render();
    });
    lightFolder.add(config, 'specular').onChange(() => {
      render();
    });
    lightFolder.addColor(config, 'ambientColor').onChange(() => {
      config.ambientColorValue[0] = config.ambientColor[0] / 255;
      config.ambientColorValue[1] = config.ambientColor[1] / 255;
      config.ambientColorValue[2] = config.ambientColor[2] / 255;
      render();
    });
    lightFolder.addColor(config, 'lightColor').onChange(() => {
      config.lightColorValue[0] = config.lightColor[0] / 255;
      config.lightColorValue[1] = config.lightColor[1] / 255;
      config.lightColorValue[2] = config.lightColor[2] / 255;
      render();
    });
    lightFolder.add(config, 'lightDirectionX', -10, 10, 0.1).onChange(() => {
      setLightDirection();
      render();
    });
    lightFolder.add(config, 'lightDirectionY', -10, 10, 0.1).onChange(() => {
      setLightDirection();
      render();
    });
    lightFolder.add(config, 'lightDirectionZ', -10, 10, 0.1).onChange(() => {
      setLightDirection();
      render();
    });
  </script>
</body>
</html>